package org.liangsy.rest.user.test;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 高效批量插入MySQL演示类（千万级数据场景优化方案）
 *
 * 本方案综合运用以下核心技术实现高性能数据写入：
 * 1. 【连接池技术】采用HikariCP作为数据库连接池，通过预先建立并维护一定数量的数据库连接，
 *    避免频繁创建/销毁连接带来的性能损耗。连接池机制能够：
 *    - 复用TCP连接，减少三次握手开销
 *    - 统一管理连接状态，防止泄漏
 *    - 自动重连机制保证可用性
 *    配置参数maximumPoolSize=线程数+2，确保线程竞争时仍有可用连接
 *
 * 2. 【JDBC批处理优化】通过rewriteBatchedStatements=true参数启用MySQL驱动批处理重写功能：
 *    - 将多个INSERT语句自动合并为多值语法：INSERT INTO table VALUES (..),(..)
 *    - 减少网络传输次数，单次批量提交可降低90%的网络IO
 *    - 结合useServerPrepStmts=false关闭服务端预处理，避免预编译语句带来的额外开销
 *
 * 3. 【事务批量提交】采用手动事务控制策略：
 *    - setAutoCommit(false)关闭自动提交，避免每条INSERT产生独立事务
 *    - 每50,000条数据执行commit()，平衡内存使用与事务日志刷盘频率
 *    - 大事务提交策略可减少redo log的fsync操作次数，提升磁盘IO效率
 *
 * 4. 【多线程并行写入】基于ExecutorService构建线程池实现：
 *    - 数据分片：将1000万记录均匀拆分到10个线程处理（每个线程100万）
 *    - 线程数选择：设置为接近CPU物理核心数×2，充分利用多核优势
 *    - 无锁设计：每个线程处理独立数据区间，避免同步竞争
 *
 * 5. 【内存管理优化】
 *    - 使用try-with-resources自动释放连接，防止内存泄漏
 *    - 每批次执行后clearBatch()及时释放Statement占用的内存
 *    - 合理设置BATCH_SIZE=50,000，平衡内存占用与批处理效率
 *
 * 6. 【MySQL服务端优化】需配合以下服务端配置：
 *    - innodb_buffer_pool_size=4G 提升缓冲池命中率
 *    - innodb_flush_log_at_trx_commit=0 批量插入期间降低日志刷盘频率
 *    - max_allowed_packet=256M 支持大体积批处理语句
 */
public class OptimizedBulkInsert {
    // HikariCP连接池配置（核心组件）
    private static final HikariDataSource dataSource;
    static {
        HikariConfig config = new HikariConfig();
        // JDBC URL关键参数组（性能优化核心）
        config.setJdbcUrl("jdbc:mysql://localhost:3306/test?"
                + "rewriteBatchedStatements=true&"  // 批处理语句重写（必须启用）
                + "useServerPrepStmts=false&"       // 禁用服务端预处理（批处理场景需关闭）
                + "cachePrepStmts=true&"            // 客户端缓存预处理语句
                + "useCompression=true&"            // 启用传输压缩（高吞吐时有效）
                + "useSSL=false&"                   // 禁用加密提升传输速度
                + "serverTimezone=UTC");            // 统一时区避免转换错误
        config.setUsername("root");
        config.setPassword("123456");
        // 连接池大小计算公式：线程数 + 2（确保线程等待时有备用连接）
        config.setMaximumPoolSize(12);
        dataSource = new HikariDataSource(config);
    }

    // 运行参数配置区（需根据硬件配置调整）
    private static final int TOTAL_INSERTS = 10_000_000; // 总数据量（科学计数法更易读）
    private static final int THREAD_COUNT = 10;          // 并发线程数（建议=CPU核心数×2）
    private static final long START_VALUE = 9_885_688;   // 起始值（模拟业务场景）
    private static final int BATCH_SIZE = 50_000;        // 批次大小（经验值：内存占用与吞吐的平衡点）


    public static void main(String[] args) throws InterruptedException {
        final LocalDateTime startTime = LocalDateTime.now();
        System.out.println("[Start] " + startTime);

        // 创建固定大小线程池（建议CPU核心数×2）
        final ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);

        // 计算每个线程处理的数据范围
        final int perThread = TOTAL_INSERTS / THREAD_COUNT;
        for (int i = 0; i < THREAD_COUNT; i++) {
            final long start = START_VALUE + i * perThread;
            // 最后一个线程处理剩余数据
            final long end = (i == THREAD_COUNT - 1) ?
                    START_VALUE + TOTAL_INSERTS : start + perThread;

            // 提交插入任务到线程池
            executor.submit(() -> batchInsert(start, end));
        }

        // 优雅关闭线程池
        executor.shutdown();
        // 等待所有任务完成（最大等待1小时）
        executor.awaitTermination(1, TimeUnit.HOURS);

        // 计算并输出总耗时
        System.out.println("[Total] " + (System.currentTimeMillis() - startTime.toLocalTime().toSecondOfDay()*1000L) + " ms");
        System.out.println("[End] " + LocalDateTime.now());
    }


    /**
     * 核心批处理方法（技术实现要点）：
     * 1. 【预处理语句复用】每个线程持有一个PreparedStatement实例，利用缓存机制避免重复解析SQL
     * 2. 【内存批处理队列】通过addBatch()将数据存入JDBC客户端缓冲区，直到达到阈值才物理写入
     * 3. 【流水线优化】批处理执行期间，下一批数据添加与当前批网络传输可并行（TCP协议栈自动处理）
     * 4. 【异常恢复机制】catch块中应实现错误记录与断点续传逻辑（示例简化了异常处理）
     */
    private static void batchInsert(long startVal, long endVal) {
        final String SQL = "INSERT INTO test_table (test_name, tast_key1, tast_key2, tast_key3) VALUES (?,?,?,?)";

        // try-with-resources语法确保Connection/Statement正确关闭（即使发生异常）
        try (Connection conn = dataSource.getConnection();
             PreparedStatement ps = conn.prepareStatement(SQL)) {

            // 事务控制三部曲：关闭自动提交 -> 批量执行 -> 手动提交
            conn.setAutoCommit(false);

            int counter = 0;
            for (long i = startVal; i < endVal; i++) {
                String val = String.valueOf(i);
                // 参数绑定采用字符串形式（实际生产环境应根据字段类型选择setInt/setDate等方法）
                ps.setString(1, val);
                ps.setString(2, val);
                ps.setString(3, val);
                ps.setString(4, val);
                ps.addBatch(); // 加入批处理队列（内存操作，不涉及网络）

                // 批次提交控制（内存与吞吐的权衡点）
                if (++counter % BATCH_SIZE == 0) {
                    ps.executeBatch();  // 将内存队列中的数据发送到数据库
                    conn.commit();     // 提交事务（触发redo log写入）
                    ps.clearBatch();   // 清空客户端批处理队列（防止内存泄漏）
                    System.out.printf("Thread %s: Committed %,d records%n",
                            Thread.currentThread().getId(), counter);
                }
            }

            // 最终提交剩余数据（避免最后小批次数据丢失）
            ps.executeBatch();
            conn.commit();
        } catch (SQLException e) {
            // 生产环境应记录失败区间并加入重试队列（此处简化处理）
            System.err.println("Batch insert failed: " + e.getMessage());
            e.printStackTrace();
        }
    }
}